package mpicbg.models;

import java.util.Random;
import java.util.ArrayList;
import java.util.Vector;
import java.util.Collection;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;


public class Tile
{
	final private float width;
	final public float getWidth(){ return width; }
	
	final private float height;
	final public float getHeight(){ return height; }
	

	private float[] lc;
	
	
	private float[] wc;
	public float[] getWC() { return wc; }

	
	private Model model;
	final public Model getModel() { return model; }
	
	
	final private ArrayList< PointMatch > matches = new ArrayList< PointMatch >();
	final public ArrayList< PointMatch > getMatches() { return matches; }
	
	
	final private ArrayList< Tile > connectedTiles = new ArrayList< Tile >();
	final public ArrayList< Tile > getConnectedTiles() { return connectedTiles; }
	
	final public boolean addConnectedTile( Tile t )
	{
		if ( connectedTiles.contains( t ) ) return true;
		else return connectedTiles.add( t );
	}
	
	
	final public boolean removeConnectedTile( Tile t )
	{
		return connectedTiles.remove( t );
	}
	
	private double error;
	final public double getError() { return error; }
	
	private double distance;
	final public double getDistance() { return distance; }
	
	public Tile(
			float width,
			float height,
			Model model )
	{
		this.width = width;
		this.height = height;
		this.model = model;
		
		lc = new float[]{ width / 2.0f - 1.0f, height / 2.0f - 1.0f };
		wc = new float[]{ lc[ 0 ], lc[ 1 ] };
	}
	
	public Tile(
			double width,
			double height,
			Model model )
	{
		this.width = ( float )width;
		this.height = ( float )height;
		this.model = model;
		
		lc = new float[]{ this.width / 2.0f - 1.0f, this.height / 2.0f - 1.0f };
		wc = new float[]{ lc[ 0 ], lc[ 1 ] };
	}
	
	final public boolean addMatches( Collection< PointMatch > more )
	{
		return matches.addAll( more );
	}
	
	
	final public boolean addMatch( PointMatch match )
	{
		return matches.add( match );
	}
	
	
	final public void update()
	{
		wc = model.apply( lc );
		
		double d = 0.0;
		double e = 0.0;
		
		int num_matches = matches.size();
		if ( num_matches > 0 )
		{
			double sum_weight = 0.0;
			for ( PointMatch match : matches )
			{
				match.apply( model );
				double dl = match.getDistance();
				d += dl;
				e += dl * dl * match.getWeight();
				sum_weight += match.getWeight();
			}
			d /= num_matches;
			e /= sum_weight;
		}
		distance = ( float )d;
		error = ( float )e;
		model.setError( e );
	}
	
	final public void updateByStrength()
	{
		wc = model.apply( lc );
		
		double d = 0.0;
		double e = 0.0;
		
		int num_matches = matches.size();
		if ( num_matches > 0 )
		{
			double sum_weight = 0.0;
			for ( PointMatch match : matches )
			{
				match.applyByStrength( model );
				double dl = match.getDistance();
				d += dl;
				e += dl * dl * match.getWeight();
				sum_weight += match.getWeight();
			}
			d /= num_matches;
			e /= sum_weight;
		}
		distance = ( float )d;
		error = ( float )e;
		model.setError( e );
	}
	
	final public boolean diceBetterModel( int max_num_tries, float scale )
	{
		Model old_model = model;
		
		for ( int t = 0; t < max_num_tries; ++t )
		{
			model = model.clone();
			model.shake( matches, scale, lc );
			update();
			if ( model.betterThan( old_model ) )
			{
				return true;
			}
			else model = old_model;
		}
		update();
		return false;
	}
	
	final public void fitModel() throws NotEnoughDataPointsException
	{
		model.fit( matches );
	}
	
	final private int traceConnectedGraph( ArrayList< Tile > graph )
	{
		graph.add( this );
		for ( Tile t : connectedTiles )
		{
			if ( !graph.contains( t ) )
				t.traceConnectedGraph( graph );
		}
		return graph.size();
	}
	
	final public void connect(
			Tile o,
			Collection< PointMatch > matches )
	{
//		float num_matches = ( float )matches.size();
//		for ( PointMatch m : matches )
//			m.setWeight( 1.0f / num_matches );
		
		this.addMatches( matches );
		o.addMatches( PointMatch.flip( matches ) );
		
		this.addConnectedTile( o );
		o.addConnectedTile( this );
	}
	
	final static public ArrayList< ArrayList< Tile > > identifyConnectedGraphs(
			Collection< Tile > tiles )
	{
		ArrayList< ArrayList< Tile > > graphs = new ArrayList< ArrayList< Tile > >();
		int numInspectedTiles = 0;
A:		for ( Tile tile : tiles )
		{
			for ( ArrayList< Tile > knownGraph : graphs )
				if ( knownGraph.contains( tile ) ) continue A; 
			ArrayList< Tile > current_graph = new ArrayList< Tile >();
			numInspectedTiles += tile.traceConnectedGraph( current_graph );
			graphs.add( current_graph );
			if ( numInspectedTiles == tiles.size() ) break;
		}
		return graphs;
	}
	
	final public boolean isInside( float[] point ) throws Exception
	{
		float[] local = model.applyInverse( point );
		return (
				local[ 0 ] >= 0.0f && local[ 0 ] < width &&
				local[ 1 ] >= 0.0f && local[ 1 ] < height );
	}
	
	final public boolean intersects( Tile t ) throws Exception
	{
		float[] p = new float[]{ 0.0f, 0.0f };
		model.applyInPlace( p );
		if ( t.isInside( p ) ) return true;
		
		p = new float[]{ width, 0.0f };
		model.applyInPlace( p );
		if ( t.isInside( p ) ) return true;
		
		p = new float[]{ width, height };
		model.applyInPlace( p );
		if ( t.isInside( p ) ) return true;
		
		p = new float[]{ 0.0f, height };
		model.applyInPlace( p );
		if ( t.isInside( p ) ) return true;
		
		Model m = t.getModel();
		
		p = new float[]{ 0.0f, 0.0f };
		m.applyInPlace( p );
		if ( isInside( p ) ) return true;
		
		p = new float[]{ t.width, 0.0f };
		m.applyInPlace( p );
		if ( isInside( p ) ) return true;
		
		p = new float[]{ t.width, t.height };
		m.applyInPlace( p );
		if ( isInside( p ) ) return true;
		
		p = new float[]{ 0.0f, t.height };
		m.applyInPlace( p );
		if ( isInside( p ) ) return true;
		
		return false;
	}
}
